﻿using System;
using System.IO;
using System.Linq;
using MLSharp.Classification;
using MLSharp.Evaluation;
using MLSharp.Filters;
using MLSharp.Utilities;
using MLSharp.Weka;
using Plossum.CommandLine;

namespace MLSharp.ConsoleRunner
{
	/// <summary>
	/// This is a much more flexible version of the test harness.  
	/// </summary>
	public class ConsoleRunnerApplication
	{
		#region Logger

		/// <summary>
		/// The logger for the harness.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Static Methods

		/// <summary>
		/// Prints a user-friendly set of instructions on how to invoke
		/// the harness.
		/// </summary>
		private static void PrintUsage()
		{
			mLogger.DebugFormat("Usage: {0} -input filename -output filename -target attributeName",
			                    Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location));
			mLogger.Debug("      [-include attribute1[,attribute2]*] [-randomize] [-iterations number]");
			mLogger.Debug("      -classifier {ClassifierName [options[,options]*]*}[,-classifier {ClassifierName [options[,options]*]*}]*");
			mLogger.Debug(" -input\t\t\tThe name of the excel data set to use.");
			mLogger.Debug(" -output\t\t\tThe name of the CSV file to write results to.");
			mLogger.Debug(" -target\t\t\tThe name of the target attribute.");
			mLogger.Debug(" -include\t\t\tA comma separated list of attributes to include.  All attributes are included by default.");
			mLogger.Debug(" -randomize\t\t\tIf set, the data set will be randomly shuffled each iteration.");
			mLogger.Debug(" -iterations\t\t\tThe number of iterations to perform.");
			mLogger.Debug(" -classifier\t\t\tA classifier to use.  Each classifier has the following sub-parameters:");
			mLogger.Debug("  ClassifierName\t\t\tThe name of the classifier (ex: See5DecisionTree).");
			mLogger.Debug("  options\t\t\tZero or more options to pass to the classifier. ");
			mLogger.Debug("\t\t\t\tSupported and required options are classifier-specific.");
		}

		#endregion

		#region Public Static Methods

		/// <summary>
		/// Creates an instance of the flexible harness and invokes it.
		/// </summary>
		/// <param name="args"></param>
		public static void Main(string[] args)
		{
			ConsoleRunnerApplication app = new ConsoleRunnerApplication {mInteractive = true};
			app.Run();
		}

		#endregion

		#region Private Fields

		/// <summary>
		/// Flag that indicates whether or not the harness is being run interactively,
		/// meaning a human launched the app and is waiting for it to do something.
		/// </summary>
		private bool mInteractive;

		/// <summary>
		/// The loader to use.
		/// </summary>
		private readonly ClassifierFactoryLoader mLoader = new ClassifierFactoryLoader();

		/// <summary>
		/// The options for running the harness.
		/// </summary>
		private HarnessOptions mOptions;

		/// <summary>
		/// The command-line parser.
		/// </summary>
		private CommandLineParser mParser;

		#endregion

		#region Private Methods

		/// <summary>
		/// If run interactively, the harness will pause until the user
		/// presses a key on their keyboard.
		/// </summary>
		private void Pause()
		{
			if (mInteractive)
			{
				mLogger.Info("Press any key to continue.");
				Console.ReadKey();
			}
		}

		/// <summary>
		/// Loads a classifier based on the specified info.
		/// </summary>
		/// <param name="classifierInfo">The info as specified via a "-classifier"
		/// command-line argument.</param>
		/// <returns>A classifier factory or null if the info 
		/// is invalid.</returns>
		private IClassifierFactory LoadClassifierFactory(string classifierInfo)
		{
			IClassifierFactory factory;

			try
			{
				factory = mLoader.Load(classifierInfo);
			}
			catch(Exception ex)
			{
				if (mLogger.IsErrorEnabled)
				{
					mLogger.Error("An error has occured while loading a classifier.", ex);
				}

				factory = null;
			}

			return factory;
		}

		/// <summary>
		/// Instantiates the specified harness type.
		/// </summary>
		/// <param name="harnessType"></param>
		/// <param name="classifierFactories"></param>
		/// <returns></returns>
		private static IHarness GetHarness(HarnessType harnessType, IClassifierFactory[] classifierFactories)
		{
			switch (harnessType)
			{
				case HarnessType.CrossValidation:
					return new CrossValidationHarness(classifierFactories);
				case HarnessType.NestedCrossValidation:
					return new NestedCrossValidationHarness(classifierFactories);
				case HarnessType.Simple:
					return new SimpleClassificationHarness(classifierFactories);
				default:
					throw new InvalidOperationException("Unexpected harness type: " + harnessType);
			}
		}

		/// <summary>
		/// Runs the flexible harness with the specified command-line options.
		/// </summary>
		private void RunInternal()
		{
			if (!log4net.LogManager.GetAllRepositories()[0].Configured)
			{
				log4net.Config.XmlConfigurator.Configure();
			}

			mLogger.Debug(mParser.UsageInfo.GetHeaderAsString(78));

			if (mParser.HasErrors)
			{
				mLogger.Debug(mParser.UsageInfo.GetErrorsAsString(78));
				mLogger.Debug(mParser.UsageInfo.GetOptionsAsString(78));
				Pause();
				return;
			}

			mLogger.DebugFormat("Reading data set from {0}...", Path.GetFullPath(mOptions.InputFile));

			IDataSet dataSet = DataSetLoader.Load(mOptions.InputFile);

			//Find the index of the target attribute
			dataSet.TargetAttributeIndex = dataSet.Attributes.FindIndex(a => a.Name == mOptions.TargetAttribute);

			if (dataSet.TargetAttributeIndex == -1)
			{
				Console.WriteLine("**Error, target attribute '{0}' not found.", mOptions.TargetAttribute);
				Console.WriteLine("  Be sure the target attribute is in the list of allowed attributes.");
				PrintUsage();
				Pause();
				return;
			}

			//Load all filters
			IDataSetFilter[] filters;

			try
			{
				filters = FilterLoader.LoadAll(mOptions.Filters.ToArray());
			}
			catch (Exception ex)
			{
				mLogger.Fatal("Error loading filters.", ex);

				PrintUsage();
				Pause();
				return;
			}

			//Load all result writers
			IResultWriter[] resultWriters;

			try
			{
				resultWriters = ResultWriterLoader.LoadAll(mOptions.ResultWriters.ToArray());
			}
			catch (Exception ex)
			{
				mLogger.Fatal("Error loading result writers.", ex);

				PrintUsage();
				Pause();
				return;
			}

			//Load classifiers
			IClassifierFactory[] classifierFactories = new IClassifierFactory[mOptions.Classifiers.Count];

			for (int i = 0; i < classifierFactories.Length; i++)
			{
				classifierFactories[i] = LoadClassifierFactory(mOptions.Classifiers[i]);

				if (classifierFactories[i] == null)
				{
					if (mLogger.IsFatalEnabled)
					{
						mLogger.FatalFormat("Unable to load classifier {0}.", mOptions.Classifiers[i]);
					}
					PrintUsage();
					Pause();
					return;
				}
			}

			for (int i = 0; i < mOptions.Iterations; i++)
			{
				mLogger.InfoFormat("-----------Iteration {0}------------", i + 1);

				DateTime startTime = DateTime.Now;

				//Initialize all the writers
				foreach (IResultWriter writer in resultWriters)
				{
					writer.Initialize();
				}

				//Prepare the harness
				IHarness harness = GetHarness(mOptions.HarnessType, classifierFactories);
				harness.ResultWriters.AddRange(resultWriters);
				harness.Filters.AddRange(filters);
				harness.RunInParallel = mOptions.RunInParallel;

				//Run it!
				harness.Run(dataSet.Clone());

				//Cleanup.  If we're running multiple iterations,
				//check the result writers, and if any of them
				//write output files, rename them.
				if (mOptions.Iterations > 1)
				{
					foreach (IResultWriter writer in resultWriters)
					{
						IFileResultWriter fileWriter = writer as IFileResultWriter;

						if (fileWriter != null)
						{
							string newFileName = fileWriter.OutputFileName.AppendFilenameSuffix((i + 1).ToString());

							File.Move(fileWriter.OutputFileName, newFileName);
						}
					}
				}

				double seconds = (DateTime.Now - startTime).TotalSeconds;

				mLogger.Info("------------------------------");
				mLogger.InfoFormat("Execution time (seconds): {0}", seconds);
			}

			Pause();
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Runs the application using arguments from the command line.
		/// </summary>
		public void Run()
		{
			mOptions = new HarnessOptions();

			mParser = new CommandLineParser(mOptions);
			mParser.Parse();

			RunInternal();
		}

		/// <summary>
		/// Runs the harness with the specified arguments.  This is the entry point for unit tests.
		/// </summary>
		/// <param name="args"></param>
		public void Run(string[] args)
		{
			mOptions = new HarnessOptions();

			mParser = new CommandLineParser(mOptions);
			mParser.Parse(args);

			RunInternal();
		}

		#endregion
	}
}
